using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace VerticalProcesses
{
    /// <summary>
    /// This abstract class describes an abstract form of Grouped Response Unit (GRU).
    /// Concrete implemetations such as forests, clearcuts, glaciers, etc. derive from
    /// this class
    /// </summary>
    public abstract class GRUClass
    {
          
        // test a variable for debugging
        protected double _testV1 = 0.0;
        protected double _testV2 = 0.0;

        //reference variables for interface types
        //these interfaces are used to chose different formulations
        //for hydrological processes and compartments

        internal EvaporationEquations evaporationEq;
        internal SnowpackEnergyInputEq snowpackEnergyInputEq;      
        internal SnowpackType snowPack;
        internal SoilType soil;
        
        
        #region GRUClass Properties

        public string GRUID { get; set; }     
        public double Elevation { get; set; }       
        public double Area { get; set; }   
        public double Latitude { get; set; }
        public double Longitude { get; set; }
        public double TimeZone { get; set; }
        public double Slope { get; set; }
        public double Aspect { get; set; }       
        public string MetStation { get; set; }
        public Dictionary<string,double> GruGlobalParameters { get; set; }
        public Dictionary<string,double> FluxStateVars { get; set; }// Returns the GRU state variables for the current simulation time step
        public bool Write { get; set; }// Returns true if GRU should be written to output and false if not
        public double Precipitation { get; set; }
        public double Throughfall { get; set; }
        public double ActualEvaporation { get; set; }
        public double PotentialEvaporation { get; set; }
        public double CurrentEvaporation { get; set; }
        public double IntSnow { get; set; }
        public double IntRain { get; set; }
        public double WaterRelease { get; set; } // Returns inflow onto soil from GRU
        public double Infiltration { get; set; }
        public double TimeStepHrs { get; set; }
        public double TimeStepSec { get; set; }
        public double CrownClosure { get; set; }
       
        #endregion

        #region GRUClass Methods

        /// <summary>
        /// Initializes GRU based on initial conditions provided. Assumes that
        /// GRU 
        /// </summary>
        /// <param name="InitialConditions"></param>
        public abstract void Initialize(Dictionary<string,double> initialConditions);
        
        /// <summary>
        /// Updates GRU state for given time step. This is a final method template with
        /// one hook
        /// </summary>
        /// <param name="TimeStep">DateTime of simulation timestep</param>
        /// <param name="METForcing">Meteorological forcing for GRU</param>
        public virtual void Update(DateTime timeStep, Dictionary<string, double> gruForcing)
        {
            // extract forcing variables
            double gruTa = gruForcing["ta"];
            double gruTmin = gruForcing["tmin"];
            double gruTmax = gruForcing["tmax"];
            double gruRain = gruForcing["rain"];
            double gruSnow= gruForcing["snow"];
            double gruShortRad =gruForcing["shortrad"]; 
            
            
            // initialize flux variables
            Precipitation = gruRain + gruSnow;
            ActualEvaporation = 0.0;
            Infiltration = 0.0;
            WaterRelease = 0.0;


            PotentialEvaporation = evaporationEq.Evaporate(timeStep, TimeStepHrs, gruTa, gruTmin, gruTmax, Latitude, Longitude, Elevation, TimeZone, Slope, Aspect);
            CurrentEvaporation = PotentialEvaporation;
            //account for canopy if there is one (virtual method)
            AccountForCanopy(timeStep,ref gruRain, ref gruSnow, ref gruShortRad);
            _testV1 = gruShortRad;
            _testV2 = gruShortRad * 1.0 / (3600.0 * TimeStepHrs) * 1.0E6; 
            
            //snow processes
            if (gruSnow > 0.0 | snowPack.Swe > 0.0)
            {
                double snowpackEnergyInput = CalculateSnowpackEngergyInput(timeStep, gruTa, gruRain, gruShortRad); 
                UpdateSnow(gruTa, gruRain, gruSnow, snowpackEnergyInput);
                soil.Infiltration = snowPack.SnowOutflow;
            }
            else
            //no snow
            {
                soil.Infiltration = gruRain;
            }
            
            //soil processes
            soil.Update(timeStep, CurrentEvaporation,snowPack.Swe,CrownClosure);
            
            // get flux and state variables

            //_testV2 = snowPack.CcPack;
            WaterRelease = soil.WaterRelease;
            CurrentEvaporation -= soil.SoilEvaporation;
            ActualEvaporation += soil.SoilEvaporation;
            updateStateAndFlux();
            hook();


        }

        private double CalculateSnowpackEngergyInput(DateTime timeStep, double gruTa, double gruRain, double gruShortRad)
        {
            double snowpackEnergyInput = snowpackEnergyInputEq.CalcSnowpackEnergyInput(timeStep, gruTa, gruRain, gruShortRad, snowPack.TempSurf, CrownClosure);
            return snowpackEnergyInput;
        }
        /// <summary>
        /// update snow compartment or direct throughfall directly to soil 
        /// </summary>
        /// <param name="gruTa"></param>
        /// <param name="gruRain"></param>
        /// <param name="gruSnow"></param>
        /// <param name="gruShortRad"></param>
        private void UpdateSnow(double gruTa, double gruRain, double gruSnow, double snowpackEnergyInput)
        {

            // new snow falling, zero CC and zero snowLiq assumed
            // new snow get added in snowPack.Update
            if (snowPack.Swe == 0.0)
            {
                //snowPack.Swe = gruSnow;
                snowPack.TempSurf = 0.0;
                snowPack.TempPack = 0.0;
                snowPack.CcSurf = 0.0;
                snowPack.CcPack = 0.0;
            }

            //snowpack present
            // need to use this function even without snowfall to update cold content
            snowPack.Update(snowpackEnergyInput, gruRain, gruSnow, gruTa);
    
        }

  
        /// <summary>
        /// Modifies the GRU properties (through a change in parameterization), such as 
        /// a change from forest to clear cut (or vice versa). 
        /// </summary>
        /// <param name="TimeStep"></param>
        /// <param name="Parameters"></param>
        /// <remarks>
        /// It is important to ensure that in the change in GRU representation that no
        /// water is lost inadvertently. Such as a shift from forest to clearcut, any water
        /// stored in the canopy must be added to the snowpack or soil.
        /// </remarks>
        public abstract void Change(Dictionary<string,double> Parameters);

        #endregion

        public GRUClass(double timeStepHrs, string GRUID, double latitude, double longitude, double elevation, double area, double slope, double aspect, double timeZone, bool write, string metStation, string evapoEq, string snowEngergyInputEq, string canopyType, string snowpackType, string soilType, Dictionary<string, double> gruGlobalParameters, Dictionary<string, double> gruLocalParameters)
        {
            this.TimeStepHrs = timeStepHrs;
            this.TimeStepSec = timeStepHrs * 60 * 60;
            this.GRUID= GRUID;
            this.Latitude = latitude;
            this.Longitude = longitude;
            this.Elevation = elevation;
            this.Area = area;
            this.Slope = slope;
            this.Aspect = aspect;
            this.TimeZone = timeZone;
            this.GruGlobalParameters = gruGlobalParameters;
            this.Write = write;
            this.MetStation = metStation;


            // I should encapsulate this in a factory, but will wait until we know where to go with YAM
            // retrieve parameters out of Dictionary
     
            this.CrownClosure = gruLocalParameters["crownclosure"];
          
            double AF = gruGlobalParameters["af"];
            double BF = gruGlobalParameters["bf"];
            double CF = gruGlobalParameters["cf"];
            double CCRF = gruGlobalParameters["ccrf"];
            double ALPHAC = gruGlobalParameters["alphac"];
            double T0 = gruGlobalParameters["t0"];
            double FC = gruGlobalParameters["fc"];
            double LP = gruGlobalParameters["lp"];
            double BETA = gruGlobalParameters["beta"];
            double MAXLIQ = gruGlobalParameters["whc"];
            double MAXSWESURF = gruGlobalParameters["maxswesurf"];

            //initial values
            double IniSwe = gruLocalParameters["iniswe"];

             //interfaces for hydrological formulations
             //these processes can be instantiated at runtime by giving a keyword
            switch (evapoEq.ToUpper())
            {
                case "HAMON": evaporationEq = new Hamon(); break;
                case "PENMAN": evaporationEq = new PenmanMonteith(); break;
                default: throw new NotImplementedException();
            }

            switch (snowEngergyInputEq.ToUpper())
            {
                case "JOSTMOORE": snowpackEnergyInputEq = new JostMoore(AF, BF, CF, CCRF, T0); break;
                case "DEGREEDAY": snowpackEnergyInputEq = new DegreeDay(AF, BF, CF, CCRF, T0); break;
                case "KUSTAS": snowpackEnergyInputEq = new Kustas(AF, BF, CF, CCRF, T0); break;
                case "HBVEC": snowpackEnergyInputEq = new HBVEC(AF, BF, CF, CCRF, T0, ALPHAC, Slope, Aspect); break;
                default: throw new NotImplementedException();
            }

            switch (snowpackType.ToUpper())
            {
                case "2LAYER": snowPack = new SnowpackTwoLayer(MAXLIQ,MAXSWESURF,IniSwe); break;
                default: throw new NotImplementedException();
            }
            switch (soilType.ToUpper())
            {
                case "HBVEC": soil = new HBVECSoil(FC, LP, BETA); break;
                case "WETLAND": soil = new WetLand(BETA); break;
                default: throw new NotImplementedException();
            }            
        }

   
        /// <summary>
        /// Accounts for potential canopy effects
        /// </summary>
        protected virtual void AccountForCanopy(DateTime timeStep,ref double gruRain, ref double gruSnow, ref double gruShortRad)
        {
        }



        /// <summary>
        /// This is a hook. Derived gru types are free to override this, but dont have to.
        /// </summary>
        protected void hook() { }

  
        /// <summary>
        /// 
        /// </summary>
        private void updateStateAndFlux()
        {
            FluxStateVars = new Dictionary<string, double>();
            FluxStateVars.Add("precipitation", Precipitation);
            FluxStateVars.Add("throughfall", Throughfall);
            FluxStateVars.Add("etpot", PotentialEvaporation);
            FluxStateVars.Add("etact", ActualEvaporation);
            FluxStateVars.Add("swe", snowPack.Swe);
            FluxStateVars.Add("coldcontent", snowPack.CcSurf);
            FluxStateVars.Add("snowmelt", soil.Infiltration);
            FluxStateVars.Add("waterrelease", WaterRelease);
            FluxStateVars.Add("soilwaterstorage", soil.SoilMoisture);
            FluxStateVars.Add("intsnow",IntSnow);
            FluxStateVars.Add("intrain", IntRain);
            FluxStateVars.Add("testV1", _testV1);
            FluxStateVars.Add("testV2", _testV2);
        }
        /// <summary>
        /// Returns Parameter (1/day) prorated to the timestep
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private double GetProratedParameter(double value)
        {
            if (TimeStepSec != 86400)
            {
                return value / 86400 * TimeStepSec;
            }
            return value;
        }
    }
}
